package com.vision.sales.sv.serv;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import com.vision.core.cm.db.data.Company;
import com.vision.core.cm.exception.RpcException;
import com.vision.core.cm.util.DateUtils;
import com.vision.core.sv.db.mgr.IDataManager;
import com.vision.core.sv.db.mgr.ISimpleCriteria;
import com.vision.core.sv.db.mgr.SimpleCriteria;
import com.vision.core.sv.serv.util.ServletUtils;
import com.vision.core.sv.util.CollectionUtils;
import com.vision.sales.cm.data.MonthlyTotals;
import com.vision.sales.cm.data.MonthlyTotalsItem;
import com.vision.sales.cm.data.SalesPermissions;
import com.vision.sales.cm.data.TransactionStatus;
import com.vision.sales.cm.db.data.Collection;
import com.vision.sales.cm.db.data.CollectionItem;
import com.vision.sales.cm.db.data.Customer;
import com.vision.sales.cm.db.data.Product;
import com.vision.sales.cm.db.data.SalesInvoice;
import com.vision.sales.cm.db.data.Salesman;
import com.vision.sales.cm.db.data.SalesmanProduct;
import com.vision.sales.cm.serv.rpc.MonthlyTotalsService;

/**
 * NOTE: Purposely not a subclass of AbstractServiceImpl.
 *  
 * @author Mark
 *
 */
@Service("monthly_totals")
public class MonthlyTotalsServiceImpl implements MonthlyTotalsService {
	
	private static final Logger log = LoggerFactory.getLogger(MonthlyTotalsServiceImpl.class);
	
	@Autowired
	@Qualifier("salesInvoiceManager")
	private IDataManager<SalesInvoice> invoiceManager;
	
	@Autowired
	@Qualifier("collectionManager")
	private IDataManager<Collection> collectionManager;
	
	@Autowired
	@Qualifier("collectionItemManager")
	private IDataManager<CollectionItem> collectionItemManager;
	
	@Autowired
	@Qualifier("salesmanProductManager")
	private IDataManager<SalesmanProduct> salesmanProductManager;
	
	private Map<String, SalesmanProduct> commissionsMap;

	@Override
	public MonthlyTotals getMonthlyTotals(Date date, List<Company> companies, List<Customer> customers) throws RpcException {
		ServletUtils.checkPermissions(SalesPermissions.ViewMonthlyTotal);
		
		List<SalesmanProduct> salesmanProducts = salesmanProductManager.getAll();
		commissionsMap = new HashMap<String, SalesmanProduct>(salesmanProducts.size());
		for (SalesmanProduct salesmanProduct : salesmanProducts) {
			commissionsMap.put(key(salesmanProduct.getSalesman(), salesmanProduct.getProduct()), salesmanProduct);
		}
		
		List<MonthlyTotalsItem> list = new ArrayList<MonthlyTotalsItem>();
		boolean[] daysWithTransactions = new boolean[32];
		
		fillSales(date, companies, customers, list, daysWithTransactions);
		fillCollection(date, companies, customers, list, daysWithTransactions);
		
		int days = 0;
		for (boolean b : daysWithTransactions)
			if (b) days++;
		
		MonthlyTotals totals = new MonthlyTotals();
		totals.setNumberOfDays(days);
		totals.setItems(list);
		
		return totals;
	}
	
	@SuppressWarnings("deprecation")
	private void fillSales(Date date, List<Company> companies, List<Customer> customers, List<MonthlyTotalsItem> list, boolean[] daysWithTransactions) {
		log.debug("Filling sales information...");
		List<SalesInvoice> invoices = getInvoices(date, companies, customers, list);
		
		double sales = 0d, average = 0d, minimum = Double.MAX_VALUE, maximum = Double.MIN_VALUE;
		for (SalesInvoice invoice : invoices) {
			daysWithTransactions[invoice.getDate().getDate()] = true;
			
			sales += invoice.getGrossSales();
			
			if (invoice.getGrossSales() < minimum)
				minimum = invoice.getGrossSales();
			
			if (invoice.getGrossSales() > maximum)
				maximum = invoice.getGrossSales();
		}
		
		if (minimum == Double.MAX_VALUE)
			minimum = 0;
		if (maximum == Double.MIN_VALUE)
			maximum = 0;
				
		int size = invoices.size();
		average = size == 0 ? 0 : sales / size;
		
		list.add(new MonthlyTotalsItem("Sales", sales, average, minimum, maximum));		
	}
	
	@SuppressWarnings("deprecation")
	private void fillCollection(Date date, List<Company> companies, List<Customer> customers, List<MonthlyTotalsItem> list, boolean[] daysWithTransactions) {
		log.debug("Filling collections information...");
		List<Collection> collections = getCollections(date, companies, customers, list);
		
		// [x][0] = amount
		// [x][1] = minimum
		// [x][2] = maximum
		double[][] figures = new double[8][3];
		for (int i = 0; i < figures.length; i++) {
			figures[i][1] = Double.MAX_VALUE;
			figures[i][2] = Double.MIN_VALUE;
		}
		
		List<CollectionItem> collectionItems = Collections.emptyList();
		if (!collections.isEmpty()) {
			List<Long> collectionIds = CollectionUtils.listFrom(collections, "getId");
			collectionItems = getCollectionItems(collectionIds);
		}
		
		for (CollectionItem collectionItem : collectionItems) {
			daysWithTransactions[collectionItem.getDate().getDate()] = true;
			
			figures[0][0] += collectionItem.getAmount();
			setMinAndMax(figures[0], collectionItem.getAmount());
			
			figures[1][0] += collectionItem.getOverruns();
			setMinAndMax(figures[1], collectionItem.getOverruns());
			
			figures[2][0] += collectionItem.getOverpayments();
			setMinAndMax(figures[2], collectionItem.getOverpayments());
			
			figures[3][0] += collectionItem.getDiscounts();
			setMinAndMax(figures[3], collectionItem.getDiscounts());
			
			figures[4][0] += collectionItem.getRejects();
			setMinAndMax(figures[4], collectionItem.getRejects());
			
			figures[5][0] += collectionItem.getReturns();
			setMinAndMax(figures[5], collectionItem.getReturns());
			
			figures[6][0] += collectionItem.getTax();
			setMinAndMax(figures[6], collectionItem.getTax());
			
			double net = collectionItem.getAmount() + collectionItem.getOverruns() + collectionItem.getOverpayments()
							- collectionItem.getDiscounts() - collectionItem.getRejects() - collectionItem.getReturns() - collectionItem.getTax();
			
			Salesman salesman = collectionItem.getSalesInvoice().getSalesman();
			Product product = collectionItem.getSalesInvoice().getProduct();
			SalesmanProduct sp = commissionsMap.get(key(salesman, product));
			
			if (sp == null) {
				log.warn("No commission rate was found for salesman {} and product {}!", salesman.getCode(), product.getCode());
			} else {
				double commission = net * (sp.getCommissionRate() / 100d);
				log.debug("Salesman: {}, Product: {}, Rate: {}, Commission: {}", new Object[]{ salesman.getCode(), product.getCode(), sp.getCommissionRate(), commission });
				figures[7][0] += commission;
				setMinAndMax(figures[7], commission);
			}
		}
		
		// Set to zero those that were not set
		for (int i = 0; i < figures.length; i++) {
			if (figures[i][1] == Double.MAX_VALUE) figures[i][1] = 0;
			if (figures[i][2] == Double.MIN_VALUE) figures[i][2] = 0;
		}
		
		int size = collections.size();
		if (size == 0) size = 1;
		
		list.add(new MonthlyTotalsItem("Payments", figures[0][0], figures[0][0] / size, figures[0][1], figures[0][2]));		
		list.add(new MonthlyTotalsItem("Overruns", figures[1][0], figures[1][0] / size, figures[1][1], figures[1][2]));
		list.add(new MonthlyTotalsItem("Overpayments", figures[2][0], figures[2][0] / size, figures[2][1], figures[2][2]));
		list.add(new MonthlyTotalsItem("Discounts", figures[3][0], figures[3][0] / size, figures[3][1], figures[3][2]));
		list.add(new MonthlyTotalsItem("Rejects", figures[4][0], figures[4][0] / size, figures[4][1], figures[4][2]));
		list.add(new MonthlyTotalsItem("Returns", figures[5][0], figures[5][0] / size, figures[5][1], figures[5][2]));
		list.add(new MonthlyTotalsItem("Withholding Tax", figures[6][0], figures[6][0] / size, figures[6][1], figures[6][2]));
		list.add(new MonthlyTotalsItem("Commissions", figures[7][0], figures[7][0] / size, figures[7][1], figures[7][2]));
	}
	
	// Helpers %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	@SuppressWarnings("unchecked")
	private List<SalesInvoice> getInvoices(Date date, List<Company> companies, List<Customer> customers, List<MonthlyTotalsItem> list) {
		ISimpleCriteria crit = SimpleCriteria.forClass(SalesInvoice.class);
		crit.add(DateExtract.month("date", DateUtils.getCalendarMonth(date)));
		crit.add(DateExtract.year("date", DateUtils.getCalendarYear(date)));
		crit.eq("status", TransactionStatus.POSTED);
		
		if (!CollectionUtils.isNullOrEmpty(companies))
			crit.in("company.id", companies);
		
		if (!CollectionUtils.isNullOrEmpty(customers))
			crit.in("customer.id", customers);
		
		List<SalesInvoice> invoices = invoiceManager.getTemplate().findByCriteria(crit.getDetached());
		return invoices;
	}
	
	@SuppressWarnings("unchecked")
	private List<Collection> getCollections(Date date, List<Company> companies, List<Customer> customers, List<MonthlyTotalsItem> list) {
		ISimpleCriteria crit = SimpleCriteria.forClass(Collection.class);
		crit.add(DateExtract.month("date", DateUtils.getCalendarMonth(date)));
		crit.add(DateExtract.year("date", DateUtils.getCalendarYear(date)));
		crit.eq("status", TransactionStatus.POSTED);
		
		if (!CollectionUtils.isNullOrEmpty(companies))
			crit.in("company.id", companies);
		
		if (!CollectionUtils.isNullOrEmpty(customers))
			crit.in("customer.id", customers);
		
		List<Collection> collections = collectionManager.getTemplate().findByCriteria(crit.getDetached());
		return collections;
	}
	
	@SuppressWarnings("unchecked")
	private List<CollectionItem> getCollectionItems(List<Long> collectionIds) {
		ISimpleCriteria crit = SimpleCriteria.forClass(CollectionItem.class);
		crit.in("collection.id", collectionIds);
		List<CollectionItem> items = collectionItemManager.getTemplate().findByCriteria(crit.getDetached());
		return items;
	}
	
	private static String key(Salesman salesman, Product product) {
		return salesman.getId() + "-" + product.getId();
	}
	
	private static void setMinAndMax(double[] array, double amount) {
		if (amount < array[1])
			array[1] = amount;
		
		if (amount > array[2])
			array[2] = amount;
	}

}
